package ElevatorPackage;

import com.sun.xml.internal.bind.v2.TODO;

import java.util.*;

/**
 * Created by Connor Madden.
 * User: Connor Madden
 * Date: 11/08/11
 * Time: 10:27 PM
 */
public class Elevator extends Observable {

    public enum Status {
        MOVE, STOP, OPEN, WAITING
    }

    public enum ElevatorAction {
        GET_DESTINATION, WAITING, MOVE, ARRIVE_AT_FLOOR, ARRIVE_AT_DESTINATION,
        STOP, OPEN_DOOR, CLOSE_DOOR, DISEMBARK_PASSENGERS, DEPART_FLOOR
    }

    boolean hasDestination;
    int destination;

    int location;
    int prevLocation;

    Status status;

    int stepTime;
    int exchangeTime;

    Thread runThread;

    LinkedList<Integer> disembarkLocations;
    TreeMap<Integer, ArrayList<Integer>> activeCalls;

    /**
     * Constructor for the <tt>Elevator</tt> class
     *
     * @param location the location of elevator
     * @param stepTime the time between steps
     * @param exchangeTime the time taken to complete passenger exchange
     */
    public Elevator(int location, int stepTime, int exchangeTime) {
        this.location = location;
        this.stepTime = stepTime;
        this.exchangeTime = exchangeTime;
        this.status = Status.STOP;
        this.hasDestination = false;
        this.destination = location;
        this.disembarkLocations = new LinkedList<Integer>();
        this.activeCalls = new TreeMap<Integer, ArrayList<Integer>>();
    }

    /**
     * Moves the elevator one location and transfers passengers along the way using the defined rules set in the program.
     * <p>notifies observers after each change and pauses for <code>stepTime</code> after move and
     * <code>exchangeTime</code> after passenger transit.
     *
     * @return <tt>true</tt> if <code>runThread</code> is already running
     */
    public boolean run() {
        if (runThread == null || !runThread.isAlive()) {
            runThread = new Thread(new StepMove(this), this.toString());
            runThread.start();
            return false;
        } else return true;
    }

    /**
     * Returns <tt>true</tt> if there is a disembark active at specified location
     * More formally, returns <tt>true</tt> if the disembark location exists
     *
     * @param location the location
     * @return <tt>true</tt> if there is a disembark active at specified location
     */
    protected boolean hasDisembark(int location) {
        return this.disembarkLocations.contains(location);
    }

    /**
     * Returns <tt>true</tt> if there is at least one disembark location active
     * More formally, returns <tt>true</tt> if the disembark locations collection is not empty
     *
     * @return <tt>true</tt> if there is at least one disembark location active
     */
    protected boolean hasDisembarkLocation() {
        return !this.disembarkLocations.isEmpty();
    }

    public LinkedList<Integer> getDisembarkLocations() {
        return disembarkLocations;
    }

    /**
     * Returns the location of the senior(longest active) disembark location
     *
     * @return location of earliest active disembark
     */
    protected int getNextDisembarkLocation() {
        return this.disembarkLocations.peek();
    }

    /**
     * Remove the disembark setting for specified location
     *
     * @param location the location of disembark
     * @return <tt>true</tt> if specified location was set as disembark
     */
    protected boolean removeDisembarkLocation(int location) {
        return this.disembarkLocations.removeFirstOccurrence(location);
    }

    /**
     * Add disembark location and trigger move
     *
     * @param location The location of disembark
     */
    public void addDisembarkLocation(int location) {
        if (!this.disembarkLocations.contains(location)) {
            this.disembarkLocations.offer(location);
            this.run();
        }
    }

    /**
     * Returns a destination using the defined rules set in the program
     *
     * @return <tt>true</tt> value of next destination
     */
    protected int getNextDestination() {
        if (this.hasNextActiveCall(location, this.getDirection())) {
            // System check there is outstanding call from a floor in the same direction the elevator last moved
            return this.getNextActiveCallFromHere(location, this.getDirection());
        } else if (this.hasDisembarkLocation()) {
            // System check elevator has requested stops
            return this.getNextDisembarkLocation();
        } else if (this.hasNextActiveCall(location, this.getDirection() * -1)) {
            // System check there is outstanding call from a floor in the opposite direction the elevator last moved
            return this.getNextActiveCallFromHere(location, this.getDirection() * -1);
        } else if (this.hasActiveCall()) {
            return getNextActiveCall();
        }
        return -1;
        // System sets next destination to current floor
    }

    protected int getNextActiveCall() {
        return activeCalls.firstKey();
    }

    public TreeMap<Integer, ArrayList<Integer>> getActiveCalls() {
        return activeCalls;
    }

    /**
     * Returns location of furthest active call in direction of travel, returns <tt>-1</tt> if no active call in
     * direction of travel can be found.
     * More formally if there is an active call signal with a location that is preceding <code>location</code>
     *
     * @param location  the start point
     * @param direction the direction
     * @return location of furthest active call in direction of travel, returns <tt>-1</tt> if no active call in
     *         direction of travel can be found.
     * @see ElevatorPackage.Elevator#getNextActiveCallFromHere(int, int)
     */
    protected int getNextActiveCallFromHere(int location, int direction) {
        int[] a = {this.activeCalls.firstKey(), this.activeCalls.lastKey()};
        for (int anA : a) {
            int x = (anA - location);
            // x / |x|
            if (direction == x / (Math.sqrt(Math.pow(x, 2)))) return a[1];
        }
        return -1;
    }

    /**
     * Returns <tt>true</tt> if there is active call signal in <code>direction</code> starting at <code>location</code>.
     * More formally if there is an active call signal with a location that is preceding <code>location</code>
     * in <code>direction</code> then it returns <tt>true</tt>
     * <p><tt>true</tt> if <code><tt>direction</tt> == x / |x|</code> where
     * <code>x = (activeCalls.firstKey() or activeCalls.lastKey()) - <tt>location</tt></code>
     *
     * @param location  the start point
     * @param direction the direction
     * @return <tt>true</tt> if there is active call in <code>direction</code> starting at <code>location</code>
     */
    protected boolean hasNextActiveCall(int location, int direction) {
        if (this.activeCalls.size() > 0) {
            int[] a = {this.activeCalls.firstKey(), this.activeCalls.lastKey()};
            for (int anA : a) {
                int x = (anA - location);
                // x / |x|
                if (direction == x / (Math.sqrt(Math.pow(x, 2)))) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns <tt>true</tt> if there is active call signal.
     *
     * @return <tt>true</tt> if there is active call
     */
    protected boolean hasActiveCall() {
        return this.activeCalls.size() > 0;
    }

    /**
     * Returns active state of specified call signal
     *
     * @param location  the location of call signal
     * @param direction the direction of call signal
     * @return <tt>true</tt> if the specified call signal is active, <tt>false</tt> if does not exist or is inactive
     */
    protected boolean hasActiveCall(int location, int direction) {
        return this.activeCalls.containsKey(location) && (this.activeCalls.get(location).contains(direction));
    }

    /**
     * Removes the specified call signal from the active call signal collection
     *
     * @param location  the location of call signal
     * @param direction the direction of call signal
     * @return <tt>true</tt> if the specified call signal existed
     */
    protected boolean deactivateCall(int location, int direction) {
        return this.activeCalls.containsKey(location) && this.activeCalls.get(location).remove((Integer) direction);
    }

    /**
     * Removes the specified call signal from the active call signal collection
     *
     * @param location  the location of call signal
     * @return <tt>true</tt> if the specified call signal existed
     */
    public boolean deactivateCall(int location) {
        if (this.activeCalls.containsKey(location)){
            this.activeCalls.remove(location);
            return true;
        }
        else return false;
    }

    /**
     * Adds the specified call signal the active call signal collection.
     *
     * @param location  the location of call
     * @param direction direction of orientation of call
     * @return <tt>true</tt> if the specified call signal existed
     */
    public boolean activateCall(int location, int direction) {
        if (this.activeCalls.containsKey(location)) {
            if (this.activeCalls.get(location).contains(direction)) {
                return false;
            } else {
                ArrayList<Integer> l = this.activeCalls.get(location);
                l.add(direction);
                this.activeCalls.put(location, l);
                this.run();
                return true;
            }
        } else {
            ArrayList<Integer> l = new ArrayList<Integer>(2);
            l.add(direction);
            this.activeCalls.put(location, l);
            this.run();
            return true;
        }
    }

    /**
     * return the total change in position since previous location
     * <p>This is calculated on the difference between <code>location</code> and <code>prevLocation</code>
     *
     * @return the change in position after last move (<tt>-1</tt> is move down, <tt>1</tt> is move up and <tt>0</tt> is no move)
     */
    protected int getDirection() {
        int x = (this.location - this.prevLocation);
        if (x != 0) return x / x;
        else return 0;
    }

    /**
     * Calls <code>this.setChanged()</code> then <code>this.notifyObservers(arg)</code>
     *
     * @param arg any object
     * @see java.util.Observable#setChanged()
     * @see java.util.Observable#notifyObservers()
     * @see java.util.Observable
     */
    protected void setChangeAndNotifyObservers(Object arg) {
        this.setChanged();
        this.notifyObservers(arg);
    }

    public int getStepTime() {
        return stepTime;
    }

    public void setStepTime(int stepTime) {
        this.stepTime = stepTime;
    }

    public int getExchangeTime() {
        return exchangeTime;
    }

    public void setExchangeTime(int exchangeTime) {
        this.exchangeTime = exchangeTime;
    }

    public boolean isHasDestination() {
        return hasDestination;
    }

    public int getDestination() {
        return destination;
    }

    public int getLocation() {
        return location;
    }

    public int getPrevLocation() {
        return prevLocation;
    }

    public Status getStatus() {
        return status;
    }

    protected void setHasDestination(boolean hasDestination) {
        this.hasDestination = hasDestination;
    }

    protected void setDestination(int destination) {
        this.destination = destination;
    }

    protected void setStatus(Status status) {
        this.status = status;
    }

    protected void setPrevLocation(int prevLocation) {
        this.prevLocation = prevLocation;
    }

    public void setLocation(int location) {
        this.location = location;
    }
}
